﻿// <copyright file="Mapping.cs" company="Oleg V. Polikarpotchkin">
// Copyright © 2009 Oleg V. Polikarpotchkin. All Right Reserved
// </copyright>
// <author>Oleg V. Polikarpotchkin</author>
// <email>ov-p@yandex.ru</email>
// <date>2009-04-12</date>
// <summary>XSD-CLR-string type mappings.</summary>
// <revision>$Id$</revision>

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Resources;
using System.Text;
using System.Xml;
using System.Xml.Linq;

namespace OpenWPFChart.Helpers.XmlItemDataReader
{
	/// <summary>
	/// One dimension XSD-CLR-string type mapping.
	/// </summary>
	public class AxisMapping
	{
		/// <summary>
		/// Initializes a new instance of the <see cref="AxisMapping"/> class.
		/// </summary>
		/// <param name="xsdType">XSD Type Name.</param>
		/// <param name="clrType">CLR Type Name.</param>
		/// <param name="typeString">The type string.</param>
		public AxisMapping(string xsdType, string clrType, string typeString)
		{
			XsdTypeString = xsdType;
			this.clrType = string.IsNullOrEmpty(clrType) ? null : Type.GetType(clrType);
			TypeString = typeString;
		}

		/// <summary>
		/// Gets XSD atomic type name like "double" or "gMonth".
		/// </summary>
		/// <value>XSD atomic type name string.</value>
		/// <remarks>
		/// Type name string doesn't contains namespace prefix.
		/// </remarks>
		public string XsdTypeString { get; private set; }

		Type clrType;
		/// <summary>
		/// Gets the CLR type.
		/// </summary>
		/// <value>The CLR type.</value>
		public Type ClrType
		{
			get { return clrType == null ? DefaultXmlToCLRTypeMapping.FromXmlType(XsdTypeString) : clrType; }
		}

		/// <summary>
		/// Gets the "type string" assigned to this mapping like Double, Int, etc.
		/// </summary>
		/// <value>The type string.</value>
		/// <remarks>
		/// "Type string" is used in XML schema construction.
		/// </remarks>
		public string TypeString { get; private set; }

		#region object overrides
		/// <summary>
		/// Determines whether the specified <see cref="T:System.Object"/> is equal to the current <see cref="T:System.Object"/>.
		/// </summary>
		/// <param name="obj">The <see cref="T:System.Object"/> to compare with the current <see cref="T:System.Object"/>.</param>
		/// <returns>
		/// true if the specified <see cref="T:System.Object"/> is equal to the current <see cref="T:System.Object"/>; otherwise, false.
		/// </returns>
		/// <exception cref="T:System.NullReferenceException">
		/// The <paramref name="obj"/> parameter is null.
		/// </exception>
		/// <remarks>
		/// This override is required for use with the Distinct() LINQ operator.
		/// <para>In order to compare the elements, the Distinct operator uses the elements’
		/// implementation of the IEquatable<T>.Equals method if the elements
		/// implement the IEquatable<T> interface. It uses their implementation of the
		/// Object.Equals method otherwise.</para>
		/// </remarks>
		public override bool Equals(object obj)
		{
			AxisMapping other = obj as AxisMapping;
			if (other == null)
				return false;
			return XsdTypeString == other.XsdTypeString && ClrType == other.ClrType && TypeString == other.TypeString;
		}

		public override int GetHashCode()
		{
			int hashCode = XsdTypeString.GetHashCode() ^ TypeString.GetHashCode();
			if (ClrType != null)
				hashCode ^= ClrType.GetHashCode();
			return hashCode;
		}

		public override string ToString()
		{
			return string.Format("XsdType={0}, CLRype={1}, TypeString={2}"
				, XsdTypeString, ClrType, TypeString);
		}
		#endregion object overrides
	}

	/// <summary>
	/// X,Y dimensions XSD-.NET-string type mappings.
	/// </summary>
	/// <remarks>
	/// <see cref="IEquatable{Mapping}"/> interface implemented for use with the Distinct() 
	/// LINQ operator.
	/// <para>In order to compare the elements, the Distinct operator uses the elements’
	/// implementation of the IEquatable<T>.Equals method if the elements
	/// implement the IEquatable<T> interface. It uses their implementation of the
	/// Object.Equals method otherwise.</para>
	/// </remarks>
	public class Mapping : IEquatable<Mapping> 
	{
		public AxisMapping[] Axes { get; private set; }

		/// <summary>
		/// Gets the concatenation of Axes TypeStrings.
		/// </summary>
		/// <value>The type string.</value>
		/// <remarks>
		/// Returns either
		/// <list type="number">
		/// <item>If <c>Asex.Length==2 && Axes[0].TypeString == Axes[1].TypeString</c> returns 
		/// <c>Axes[0].TypeString</c>.</item>
		/// <item>Otherwise returns the concatenation of all <c>Axes[i].TypeString</c>'s.</item>
		/// </list>
		/// </remarks>
		public string TypeString
		{
			get
			{
				if (Axes.Length == 2)
					return Axes[0].TypeString == Axes[1].TypeString ? Axes[0].TypeString
						: Axes[0].TypeString + Axes[1].TypeString;

				StringBuilder sb = new StringBuilder();
				foreach (AxisMapping map in Axes)
				{
					sb.Append(map.TypeString);
				}
				return sb.ToString();
			}
		}

		/// <summary>
		/// Gets the dot-separated concatenation of Axes TypeStrings.
		/// </summary>
		/// <value>The type string.</value>
		/// <remarks>
		/// Returns either
		/// <list type="number">
		/// <item>If <c>Asex.Length==2 && Axes[0].TypeString == Axes[1].TypeString</c> returns 
		/// <c>Axes[0].TypeString</c>.</item>
		/// <item>Otherwise returns the dot-separated concatenation of all <c>Axis[i].TypeString</c>'s.</item>
		/// </list>
		/// </remarks>
		public string DotSeparatedTypeString
		{
			get
			{
				if (Axes.Length == 2)
					return Axes[0].TypeString == Axes[1].TypeString ? Axes[0].TypeString
						: Axes[0].TypeString + "." + Axes[1].TypeString;

				StringBuilder sb = new StringBuilder();
				foreach (AxisMapping map in Axes)
				{
					if (sb.Length == 0)
						sb.Append(map.TypeString);
					else
						sb.AppendFormat(".{0}", map.TypeString);
				}
				return sb.ToString();
			}
		}

		/// <summary>
		/// Loads XML-CLR-string type mappings from the <see cref="System.IO.TextReader"/> specified.
		/// </summary>
		/// <param name="mappingReader">Input Mapping XML <see cref="System.IO.TextReader"/>.</param>
		/// <returns>List of <see cref="Mapping"/> objects.</returns>
		/// <remarks>
		/// <see cref="Mapping"/> instances with recurring <see cref="TypeString"/> property 
		/// values are removed from output.
		/// </remarks>
		/// <exception cref="ValidationException"/>
		/// <exception cref="RecurringMappingEntriesException"/>
		public static List<Mapping> Load(TextReader mappingReader)
		{
			// XML Mapping Schema resource name.
			const string mappingSchemaResourceName = "xmlitemdatareader/typemappings.xsd";
			// XML namespace must be used in XML mappings files.
			const string mappingNamespaceName = "urn:ItemData-mapping";
			// Mapping element attributes.
			const string attrNameXsdType = "xsd-type"
				, attrNameClrType = "clr-type"
				, attrNameTypeString = "type-string";

			// Get xml schema stream from the "mappingSchemaResourceName" resource.
			Assembly assembly = Assembly.GetAssembly(typeof(Mapping));
			ResourceManager rm = new ResourceManager(assembly.GetName().Name + ".g", assembly);
			using (XmlTextReader schemaReader = new XmlTextReader(rm.GetStream(mappingSchemaResourceName)))
			{
				// Prepaire XmlReaderSettings for input file validation.
				XmlReaderSettings settings = new XmlReaderSettings();
				settings.ValidationType = ValidationType.Schema;
				settings.Schemas.Add(mappingNamespaceName, schemaReader);
				StringBuilder sbErrors = null;
				List<ValidationException.ValidationError> errors = null;
				settings.ValidationEventHandler += (sender, e) =>
				{
					if (sbErrors == null)
						sbErrors = new StringBuilder();
					sbErrors.AppendFormat("Validation error: {1}{0}Line={2}, position={3}{0}"
						, System.Environment.NewLine, e.Exception.Message
						, e.Exception.LineNumber, e.Exception.LinePosition);
					if (errors == null)
						errors = new List<ValidationException.ValidationError>();
					errors.Add(new ValidationException.ValidationError()
					{
						Message = e.Exception.Message,
						Line = e.Exception.LineNumber,
						Position = e.Exception.LinePosition
					});
				};

				// Load and validate the file.
				using (XmlReader reader = XmlReader.Create(mappingReader, settings))
				{
					XElement mappings = XElement.Load(reader);
					if (sbErrors != null)
						// Validation error(s) occured.
						throw new ValidationException("Mapping file validation errors"
							+ sbErrors.ToString(), errors.ToArray());

					XNamespace xns = mappingNamespaceName;
					// Check the root element name (i.e. Mappings in "urn:PointSeries-mapping" xmlns).
					if (mappings.Name != xns + "Mappings")
						throw new ValidationException(string.Format("Root element {0} missed", xns + "Items"));
					// Parse the Mapping elements.
					List<Mapping> mappingList = (from mapping in mappings.Elements(xns + "Mapping")
							 let xAxis = mapping.Element(xns + "XAxis")
							 let yAxis = mapping.Element(xns + "YAxis")
							 select new Mapping()
							 {
								 Axes = new AxisMapping[] {
									new AxisMapping((string)xAxis.Attribute(attrNameXsdType),
										(string)xAxis.Attribute(attrNameClrType),
										(string)xAxis.Attribute(attrNameTypeString)),
									new AxisMapping((string)yAxis.Attribute(attrNameXsdType),
										(string)yAxis.Attribute(attrNameClrType),
										(string)yAxis.Attribute(attrNameTypeString))
								}
							 }
						).ToList();
					// Parse the Mapping3D elements.
					IEnumerable<Mapping> mapping3DList = (from mapping in mappings.Elements(xns + "Mapping3D")
							  let xAxis = mapping.Element(xns + "XAxis")
							  let yAxis = mapping.Element(xns + "YAxis")
							  let zAxis = mapping.Element(xns + "ZAxis")
							  select new Mapping()
							  {
								  Axes = new AxisMapping[] {
									new AxisMapping((string)xAxis.Attribute(attrNameXsdType),
										(string)xAxis.Attribute(attrNameClrType),
										(string)xAxis.Attribute(attrNameTypeString)),
									new AxisMapping((string)yAxis.Attribute(attrNameXsdType),
										(string)yAxis.Attribute(attrNameClrType),
										(string)yAxis.Attribute(attrNameTypeString)),
									new AxisMapping((string)zAxis.Attribute(attrNameXsdType),
										(string)zAxis.Attribute(attrNameClrType),
										(string)zAxis.Attribute(attrNameTypeString))
								}
							  }
						);
					mappingList.AddRange(mapping3DList);

					// Check result for recurring entries.
					List<Mapping> recurring = new List<Mapping>();
					for (int i = 0; i < mappingList.Count - 1; i++)
					{
						Mapping map = mappingList[i];
						for (int j = i + 1; j < mappingList.Count; j++)
						{
							Mapping map1 = mappingList[j];
							if (map.DotSeparatedTypeString == map1.DotSeparatedTypeString)
								recurring.Add(map1);
						}
					}
					if (recurring.Count > 0)
					{
						StringBuilder sb = new StringBuilder("Recurring entries found in the mapping file:");
						foreach (Mapping map in recurring)
						{
							sb.Append(System.Environment.NewLine + map.ToString());
						}
						throw new RecurringMappingEntriesException(sb.ToString(), recurring.ToArray());
					}

					return mappingList;
				}
			}
		}
		/// <summary>
		/// Loads XSD-.NET-string type mappings from the <see cref="System.IO.Stream"/> specified.
		/// </summary>
		/// <param name="stm">Input Mapping XML data <see cref="System.IO.Stream"/>.</param>
		/// <returns>List of <see cref="Mapping"/> objects.</returns>
		/// <remarks>
		/// <see cref="Mapping"/> instances with recurring <see cref="TypeString"/> property 
		/// values are removed from output.
		/// </remarks>
		public static List<Mapping> Load(Stream stm)
		{
			List<Mapping> mappings = null;
			using (TextReader rdr = new StreamReader(stm))
			{
				mappings = Load(rdr);
			}
			return mappings;
		}
		/// <summary>
		/// Loads XSD-.NET-string type mappings from the file specified.
		/// </summary>
		/// <param name="mappingFileName">Mapping file name.</param>
		/// <returns>List of <see cref="Mapping"/> objects.</returns>
		/// <remarks>
		/// <see cref="Mapping"/> instances with recurring <see cref="TypeString"/> property 
		/// values are removed from output.
		/// </remarks>
		public static List<Mapping> Load(string mappingFileName)
		{
			List<Mapping> mappings = null;
			using (Stream stm = File.OpenRead(mappingFileName))
			{
				mappings = Load(stm);
			}
			return mappings;
		}

		#region Object overrides
		/// <summary>
		/// Determines whether the specified <see cref="T:System.Object"/> is equal to the current <see cref="T:System.Object"/>.
		/// </summary>
		/// <param name="obj">The <see cref="T:System.Object"/> to compare with the current <see cref="T:System.Object"/>.</param>
		/// <returns>
		/// true if the specified <see cref="T:System.Object"/> is equal to the current <see cref="T:System.Object"/>; otherwise, false.
		/// </returns>
		/// <exception cref="T:System.NullReferenceException">
		/// The <paramref name="obj"/> parameter is null.
		/// </exception>
		/// <remarks>
		/// This override is required for use with the Distinct() LINQ operator.
		/// <para>In order to compare the elements, the Distinct operator uses the elements’
		/// implementation of the IEquatable<T>.Equals method if the elements
		/// implement the IEquatable<T> interface. It uses their implementation of the
		/// Object.Equals method otherwise.</para>
		/// </remarks>
		public override bool Equals(object obj)
		{
			Mapping other = obj as Mapping;
			if (other == null || Axes.Length != other.Axes.Length)
				return false;
			for (int i = 0; i < Axes.Length; i++)
			{
				if (!Axes[i].Equals(other.Axes[i]))
					return false;
			}
			return true;
		}

		public override int GetHashCode()
		{
			int hashCode = Axes[0].GetHashCode();
			for (int i = 1; i < Axes.Length; i++)
			{
				hashCode ^= Axes[i].GetHashCode();
			}
			return hashCode;
		}

		public override string ToString()
		{
			StringBuilder sb = new StringBuilder();
			for (int i = 0; i < Axes.Length; i++)
			{
				if (i == 0)
					sb.AppendFormat("Axis{0}={1}", i, Axes[i]);
				else
					sb.AppendFormat(", Axis{0}={1}", i, Axes[i]);
			}
			return sb.ToString();
		}
		#endregion Object overrides

		#region IEquatable<Mapping> Members
		public bool Equals(Mapping other)
		{
			return DotSeparatedTypeString == other.DotSeparatedTypeString;
		}
		#endregion IEquatable<Mapping> Members
	}
}
